/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using EnvDTE;
using VSLangProj;
using EnvDTE80;
using System.ComponentModel;
using System.Windows.Forms;
using System.Globalization;
using Microsoft.VisualStudio.CommandBars;
using System.Runtime.InteropServices;

namespace DaveSexton.DocProject
{
	/// <remarks>
	/// This class is not thread-safe.
	/// </remarks>
	public sealed class VSEnvironmentHost : MarshalByRefObject, IEnvironmentHost
	{
		#region Public Properties
		public static System.Windows.Forms.IWin32Window VisualStudioMainWindow
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (vsWindow == null)
				{
					vsWindow = new NativeWindow();
					vsWindow.AssignHandle((IntPtr) DTE.MainWindow.HWnd);
				}

				return vsWindow;
			}
		}

		public static string Status
		{
			get
			{
				return DTE.StatusBar.Text;
			}
			set
			{
				try
				{
					DTE.StatusBar.Text = value ?? string.Empty;
				}
				catch (COMException)
				{
					// occurred in testing when status was set by an asynchronous operation that ended after the UI started closing
				}
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased")]
		[CLSCompliant(false)]
		public static DTE2 DTE
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (dte == null)
					// Do not retrieve the DTE from the ROT anymore since the MSBuild task, when running outside of VS, cannot use the DTE.
					//dte = (DTE2) System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.8.0");
					throw new InvalidOperationException(Resources.Errors.NoDte);

				return dte;
			}
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			internal set
			{
				dte = value;
			}
		}

		[CLSCompliant(false)]
		public static OutputWindowPane BuildOutput
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (buildOutput == null)
				{
					OutputWindow window = (OutputWindow) DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput).Object;

					string build = LocalizedVS.GetCommandBarName("Build");

					try
					{
						buildOutput = window.OutputWindowPanes.Item(build);
					}
					catch (ArgumentException)
					{
						buildOutput = window.OutputWindowPanes.Add(build);
					}
				}

				return buildOutput;
			}
		}

		public static CultureInfo CurrentCulture
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (currentCulture == null)
					currentCulture = new System.Globalization.CultureInfo(DTE.LocaleID);

				return currentCulture;
			}
		}

		public static IEnumerable<Project> SelectedProjects
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return new MarshalByRefEnumerable<Project>(GetSelectedProjectsInternal);
			}
		}

		[CLSCompliant(false)]
		public static CommandBar MenuBar
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				// MenuBar does not have to be localized
				return ((CommandBars) DTE.CommandBars)["MenuBar"];
			}
		}

		public static VisualStudioInstance DefaultInstance
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				if (defaultInstance == null)
					defaultInstance = FindDefaultInstance();

				return defaultInstance;
			}
		}

		public static ICollection<VisualStudioInstance> SupportedInstances
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return supportedInstances.AsReadOnly();
			}
		}
		#endregion

		#region Private / Protected
		/// <summary>
		/// Lazy initialization has been implemented.  Use the <see cref="VisualStudioMainWindow" /> property instead of accessing this field directly.
		/// </summary>
		private static NativeWindow vsWindow;
		/// <summary>
		/// Lazy initialization has been implemented.  Use the <see cref="DTE" /> property instead of accessing this field directly.
		/// This field is assigned via the <see cref="DTE" /> property by the DocProjectEnvironment class - do not attempt to use GetActiveObject.
		/// </summary>
		private static DTE2 dte;
		/// <summary>
		/// Lazy initialization has been implemented.  Use the <see cref="BuildOutput" /> property instead of accessing this field directly.
		/// </summary>
		private static OutputWindowPane buildOutput;
		private static CultureInfo currentCulture;
		private static VisualStudioInstance defaultInstance;
		private static readonly List<VisualStudioInstance> supportedInstances = new List<VisualStudioInstance>(
			new VisualStudioInstance[]
			{
				// In order of precedence
				new VisualStudioInstance("devenv.exe", "VisualStudio", "9.0"), 
				new VisualStudioInstance("VCSExpress.exe", "VCSExpress", "9.0"), 
				new VisualStudioInstance("VBExpress.exe", "VBExpress", "9.0"), 
				new VisualStudioInstance("devenv.exe", "VisualStudio", "8.0"), 
				new VisualStudioInstance("VCSExpress.exe", "VCSExpress", "8.0"), 
				new VisualStudioInstance("VBExpress.exe", "VBExpress", "8.0"), 
			});
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="VSEnvironmentHost" /> class.
		/// </summary>
		public VSEnvironmentHost()
		{
		}
		#endregion

		#region Methods
		#region ToolBars and Menus
		/// <summary>
		/// Gets a toolbar or top-level menu bar using its English <paramref name="name"/>.  Note that not all top-level menus
		/// are attainable using this method.  Therefore, it's recommended that the <see cref="GetMenu(string)"/> method be used instead
		/// to retrieve a top-level menu.
		/// </summary>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[System.Diagnostics.DebuggerHidden]
		[CLSCompliant(false)]
		public static CommandBar GetCommandBar(string name)
		{
			if (name == null)
				throw new ArgumentNullException("name");

			CommandBar found = FindCommandBar(delegate(CommandBar bar)
			{
				return string.Equals(bar.Name, name, StringComparison.Ordinal);
			});

			if (found == null)
				throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Errors.InvalidCommandBarName, name), "name");

			return found;
		}

		/// <summary>
		/// Gets a toolbar or top-level menu bar using the specified <param name="predicate"/>.  Note that not all top-level menus
		/// are attainable using this method.  Therefore, it's recommended that the <see cref="GetMenu(string)"/> method be used instead
		/// to retrieve a top-level menu.
		/// </summary>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar FindCommandBar(Predicate<CommandBar> predicate)
		{
			if (predicate == null)
				throw new ArgumentNullException("predicate");

			foreach (CommandBar bar in (CommandBars) DTE.CommandBars)
			{
				if (predicate(bar))
					return bar;
			}

			return null;
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static void RemoveCommandBar(Predicate<CommandBar> predicate)
		{
			CommandBar bar = FindCommandBar(predicate);

			if (bar != null)
				bar.Delete();
		}

		[System.Diagnostics.DebuggerHidden]
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static Window2 GetToolWindow(Guid id)
		{
			// this collection is not localized
			return (Window2) ((Windows2) DTE.Windows).Item(id.ToString("B"));
		}

		/// <summary>
		/// Gets a top-level menu bar using its English <paramref name="name"/>.
		/// </summary>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar GetMenu(string name)
		{
			return GetMenu(MenuBar, name);
		}

		/// <summary>
		/// Gets a menu bar using its English <paramref name="name"/>.
		/// </summary>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar GetMenu(CommandBar parent, string name)
		{
			if (parent == null)
				throw new ArgumentNullException("parent");

			if (name == null)
				throw new ArgumentNullException("name");

			foreach (CommandBarControl control in parent.Controls)
			{
				if (control.Type == MsoControlType.msoControlPopup)
				{
					CommandBarPopup popup = (CommandBarPopup) control;

					if (popup.CommandBar.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
						return popup.CommandBar;
				}
			}

			return null;
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddMenu(string name)
		{
			return AddMenu(MenuBar, name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
		[CLSCompliant(false)]
		public static CommandBar AddMenu(CommandBar parent, string name)
		{
			return AddMenu(parent, name, parent.Controls.Count + 1);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddMenu(CommandBar parent, string name, int position)
		{
			if (parent == null)
				throw new ArgumentNullException("parent");

			if (name == null)
				throw new ArgumentNullException("name");

			Commands2 commands = (Commands2) DTE.Commands;

			try
			{
				return (CommandBar) commands.AddCommandBar(name, vsCommandBarType.vsCommandBarTypeMenu, parent, position);
			}
			catch (System.ArgumentException)
			// assume command already exists
			{
				return null;
			}
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddOrGetMenu(string name)
		{
			return AddMenu(name) ?? GetMenu(name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddOrGetMenu(CommandBar parent, string name)
		{
			return AddMenu(parent, name) ?? GetMenu(parent, name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddOrGetMenu(CommandBar parent, string name, int position)
		{
			return AddMenu(parent, name, position) ?? GetMenu(parent, name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddContextMenu(string name)
		{
			return AddContextMenu(null, name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
		[CLSCompliant(false)]
		public static CommandBar AddContextMenu(CommandBar parent, string name)
		{
			return AddContextMenu(parent, name, (parent != null) ? parent.Controls.Count + 1 : 0);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddContextMenu(CommandBar parent, string name, int position)
		{
			if (parent == null)
				throw new ArgumentNullException("parent");

			if (name == null)
				throw new ArgumentNullException("name");

			Commands2 commands = (Commands2) DTE.Commands;

			try
			{
				return (CommandBar) commands.AddCommandBar(name, vsCommandBarType.vsCommandBarTypePopup, parent, position);
			}
			catch (System.ArgumentException)
			// assume command already exists
			{
				return null;
			}
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddOrGetContextMenu(string name)
		{
			return AddContextMenu(name) ?? GetMenu(name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddOrGetContextMenu(CommandBar parent, string name)
		{
			return AddContextMenu(parent, name) ?? GetMenu(parent, name);
		}

		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddOrGetContextMenu(CommandBar parent, string name, int position)
		{
			return AddContextMenu(parent, name, position) ?? GetMenu(parent, name);
		}

		/// <remarks>
		/// It's important to note that this method will add the same tool bar again without throwing an exception.
		/// To ensure that a tool bar with a unique name is only added once, use the <see cref="AddOrGetToolbar"/>
		/// method instead.
		/// </remarks>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		[CLSCompliant(false)]
		public static CommandBar AddToolBar(string name, MsoBarPosition position)
		{
			CommandBars bars = (CommandBars) DTE.CommandBars;
			
			return bars.Add(name, position, null, false);
		}
		#endregion

		#region Trace, Status, Progress and Dialog
		public static void Trace(string message)
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString(message);
		}

		public static void Trace(string format, params object[] args)
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString((args == null)
				? format
				: string.Format(System.Globalization.CultureInfo.CurrentCulture, format, args));
		}

		public static void TracePath(string path)
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString(Environment.ExpandEnvironmentVariables(path));
		}

		public static void TraceLine()
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString(Environment.NewLine);
		}

		public static void TracePathLine(string path)
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString(Environment.ExpandEnvironmentVariables(path) + Environment.NewLine);
		}

		public static void TraceLine(string message)
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString(message + Environment.NewLine);
		}

		public static void TraceLine(string format, params object[] args)
		{
			// must use property, which supports lazy initialization
			BuildOutput.OutputString(((args == null)
				? format
				: string.Format(System.Globalization.CultureInfo.CurrentCulture, format, args))
				+ Environment.NewLine);
		}

		public static void ResetStatus()
		{
			DTE.StatusBar.Text = string.Empty;
		}

		public static void SetProgress(string label, int completed, int total)
		{
			DTE.StatusBar.Progress(true, (label == null) ? string.Empty : label, completed, total);
		}

		public static void ResetProgress()
		{
			ResetProgress(false);
		}

		public static void ResetProgress(bool keepStatus)
		{
			string text = (keepStatus) ? DTE.StatusBar.Text : null;

			DTE.StatusBar.Progress(false, string.Empty, 0, 0);

			if (text != null)
				DTE.StatusBar.Text = text;
		}
		#endregion

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		private static VisualStudioInstance FindDefaultInstance()
		{
			if (dte != null)
			{
				System.Reflection.Assembly assembly = System.Reflection.Assembly.GetEntryAssembly();

				if (assembly != null)		// occurred during testing in VS 2008 Standard.
				{
					string path = assembly.CodeBase;

					if (!string.IsNullOrEmpty(path))
					{
						Uri uri;
						if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out uri))
						{
							if (uri.IsAbsoluteUri)
								path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
							else
								path = System.Web.HttpUtility.UrlDecode(uri.ToString());

							string name = System.IO.Path.GetFileName(path);
							path = System.IO.Path.GetDirectoryName(path);

							return new VisualStudioInstance(name, path, dte.RegistryRoot, dte.Version);
						}
					}
				}
			}

			foreach (VisualStudioInstance instance in supportedInstances)
			{
				if (instance.Initialize())
					return instance;
			}

			return null;
		}

		private static IEnumerable<Project> GetSelectedProjectsInternal()
		{
			foreach (object obj in (System.Collections.IEnumerable) DTE.ActiveSolutionProjects)
			{
				Project project = obj as Project;

				if (project != null)
					yield return project;
			}
		}
		#endregion

		#region Events
		private readonly object ProjectSavedEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectSavedEvent;
		private readonly object ProjectOpenedEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectOpenedEvent;
		private readonly object ProjectClosedEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectClosedEvent;
		private readonly object ProjectBuildCompleteEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectBuildCompleteEvent;

		/// <summary>
		/// Event raised after a DocProject or DocSite project has been built.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectBuildComplete
		{
			add
			{
				lock (ProjectBuildCompleteEventLock)
				{
					ProjectBuildCompleteEvent += value;
				}
			}
			remove
			{
				lock (ProjectBuildCompleteEventLock)
				{
					ProjectBuildCompleteEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ProjectBuildComplete" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		internal void OnProjectBuildComplete(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectBuildCompleteEventLock)
			{
				handler = ProjectBuildCompleteEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a DocProject or DocSite project is closed.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectClosed
		{
			add
			{
				lock (ProjectClosedEventLock)
				{
					ProjectClosedEvent += value;
				}
			}
			remove
			{
				lock (ProjectClosedEventLock)
				{
					ProjectClosedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ProjectClosed" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		internal void OnProjectClosed(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectClosedEventLock)
			{
				handler = ProjectClosedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a DocProject or DocSite project is first opened.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectOpened
		{
			add
			{
				lock (ProjectOpenedEventLock)
				{
					ProjectOpenedEvent += value;
				}
			}
			remove
			{
				lock (ProjectOpenedEventLock)
				{
					ProjectOpenedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ProjectOpened" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		internal void OnProjectOpened(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectOpenedEventLock)
			{
				handler = ProjectOpenedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a DocProject or DocSite project, or one of its files, is about to be saved.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectSaved
		{
			add
			{
				lock (ProjectSavedEventLock)
				{
					ProjectSavedEvent += value;
				}
			}
			remove
			{
				lock (ProjectSavedEventLock)
				{
					ProjectSavedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="Save" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		internal void OnProjectSaved(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectSavedEventLock)
			{
				handler = ProjectSavedEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region Nested
		[ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
		private interface IOleServiceProvider
		{
			[PreserveSig]
			int QueryService([In]ref Guid guidService, [In]ref Guid riid,
				 [MarshalAs(UnmanagedType.Interface)] out System.Object obj);
		}
		#endregion

		#region IEnvironmentHost Members
		public IWin32Window MainWindow
		{
			get
			{
				return VisualStudioMainWindow;
			}
		}

		string IEnvironmentHost.Status
		{
			get
			{
				return Status;
			}
			set
			{
				Status = value;
			}
		}

		public void ShowTrace()
		{
			BuildOutput.Activate();
		}
		#endregion

		#region IServiceProvider Members
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DaveSexton.DocProject.VSEnvironmentHost+IOleServiceProvider.QueryService(System.Guid@,System.Guid@,System.Object@)")]
		public object GetService(Type serviceType)
		{
			if (serviceType == null)
				return null;

			try
			{
				IOleServiceProvider services = dte as IOleServiceProvider;

				if (services == null)
					return null;

				Guid guid = serviceType.GUID;

				if (guid == Guid.Empty)
					return null;

				object service;
				services.QueryService(ref guid, ref guid, out service);

				return service;
			}
			catch (COMException) { }

			return null;
		}
		#endregion
	}
}
